package main

import (
	"bufio"
	"bytes"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"os"
	"strings"
	"time"

	"github.com/pkg/errors"
	"github.com/spf13/cobra"

	"mydata/cmd/other"
	"mydata/internal/file"
	"mydata/internal/version"
)

var MyConvCmd = &cobra.Command{
	Use:     "myconv",
	Aliases: []string{"myconv"},
	Short:   "myconv",
	RunE:    RunIconvE,
}

var versionCmd = &cobra.Command{
	Use:   "version",
	Short: "Show version number",
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println(version.GetRawInfo())
	},
}

var defaultFormat = file.FileFormat{
	FieldsTerminated: ",",
	FieldsEnclosed:   "",
	EnclosedOptFlag:  false,
	FieldsEscaped:    "\\",
	LinesTerminated:  "\n",
}

var ff = defaultFormat

var srcSchemaFileName string
var dstSchemaFileName string

var srcFilename string
var dstFilename string
var fromCode string
var toCode string

var prefixRowStr string
var prefixRowHeader []byte
var prefixRowHeaderLen int
var debug bool
var bufSize int

var skipErr bool

func init() {
	log.SetFlags(log.LstdFlags | log.Lshortfile | log.Lmicroseconds)
	log.SetOutput(os.Stdout)

	MyConvCmd.Flags().StringVar(&srcSchemaFileName, "src-schema", "", "schema file")
	MyConvCmd.Flags().StringVar(&dstSchemaFileName, "dst-schema", "", "schema file")

	MyConvCmd.Flags().StringVarP(&fromCode, "from", "f", "", "from code")
	MyConvCmd.Flags().StringVarP(&toCode, "to", "t", "", "to code")
	MyConvCmd.Flags().StringVar(&srcFilename, "src-file", "", "source filename")
	MyConvCmd.Flags().StringVar(&dstFilename, "dst-file", "", "dest filename")
	MyConvCmd.Flags().IntVar(&bufSize, "buf-size", 32*1024, "read buffer size")

	MyConvCmd.Flags().StringVar(&ff.FieldsTerminated, "fields-terminated", defaultFormat.FieldsTerminated, "fields terminated")
	MyConvCmd.Flags().StringVar(&ff.FieldsEnclosed, "fields-enclosed", defaultFormat.FieldsEnclosed, "fields enclosed")
	MyConvCmd.Flags().StringVar(&ff.FieldsEscaped, "fields-escaped", defaultFormat.FieldsEscaped, "fields escaped")
	MyConvCmd.Flags().StringVar(&ff.LinesTerminated, "lines-terminated", defaultFormat.LinesTerminated, "lines terminated")
	//.Flags().BoolVar(&enclosedOptFlag, "enclosed-optionally", defaultFormat.EnclosedOptFlag, "fields enclosed optionally")

	MyConvCmd.Flags().StringVar(&prefixRowStr, "row-header", "", "bytes prefix row header")

	MyConvCmd.Flags().BoolVar(&debug, "debug", false, "debug flag")
	MyConvCmd.Flags().BoolVar(&skipErr, "skip-err", true, "skip error")

	MyConvCmd.Flags().SortFlags = false // 禁止flag排序

	_ = MyConvCmd.MarkFlagRequired("from")
	_ = MyConvCmd.MarkFlagRequired("to")
	_ = MyConvCmd.MarkFlagRequired("src-file")
	_ = MyConvCmd.MarkFlagRequired("dst-file")

	MyConvCmd.AddCommand(versionCmd)
}

func ExecuteIconv() {
	MyConvCmd.SilenceUsage = true

	if err := MyConvCmd.Execute(); err != nil {
		if debug {
			fmt.Printf("%+v\n", err)
		}
		os.Exit(1)
	}
}

func RunIconvE(*cobra.Command, []string) error {
	version.LogVersionInfo()

	if err := ff.AdjustAndSetFlags(); err != nil {
		return errors.WithStack(err)
	}

	if debug {
		log.Printf("ff:%+v", ff)
	}

	// 校验输入参数
	if strings.Contains(strings.ToLower(fromCode), "ibm1388") && len(srcSchemaFileName) == 0 {
		return errors.Errorf("srcSchemaFileName must set with ibm1388")
	}

	if strings.Contains(strings.ToLower(toCode), "ibm1388") && len(dstSchemaFileName) == 0 {
		return errors.Errorf("dstSchemaFileName must set with ibm1388")
	}

	if strings.HasPrefix(prefixRowStr, "0x") && len(prefixRowStr) > 2 {
		decodeString, err := hex.DecodeString(prefixRowStr[2:])
		if err != nil {
			return errors.Wrapf(err, "%v", prefixRowStr)
		}
		prefixRowHeader = decodeString
		prefixRowHeaderLen = len(prefixRowHeader)
	} else {
		prefixRowHeader = []byte(prefixRowStr)
		prefixRowHeaderLen = len(prefixRowHeader)
	}

	stat, err2 := os.Stat(srcFilename)
	if err2 != nil {
		return errors.WithStack(err2)
	}
	fileSize := stat.Size()

	log.Printf("translate file from %s to %s, code from %s to %s, src file size:%d", srcFilename, dstFilename, fromCode, toCode, fileSize)

	// 从文件获取表结构信息，文件格式为：列名,长度
	if len(srcSchemaFileName) > 0 { // 根据源端schema转目标文件
		srcRow, err := GetSchemaFromFile(srcSchemaFileName)
		if err != nil {
			return err
		}
		log.Printf("get schema, src row size:%d", srcRow.RealSize)

		if fileSize%int64(srcRow.RealSize+prefixRowHeaderLen) != 0 {
			return errors.Errorf("src file size:%d, but every row size:%d, not match", fileSize, srcRow.RealSize)
		}

		log.Printf("rowsize:%d, lines:%.2f", srcRow.RealSize+prefixRowHeaderLen, float64(fileSize/int64(srcRow.RealSize+prefixRowHeaderLen)))

		return ConvertBySrcSchema(srcFilename, dstFilename, srcRow, fromCode, toCode)
	}

	if len(dstSchemaFileName) > 0 { // 根据目标schema转文件
		dstRow, err := GetSchemaFromFile(dstSchemaFileName)
		if err != nil {
			return err
		}

		log.Printf("rowSize:%d", dstRow.RealSize)

		return ConvertByDstSchema(srcFilename, dstFilename, dstRow)
	}

	return nil
}

func ConvertBySrcSchema(srcFilename, dstFilename string, srcRow *other.Row, from, to string) error {
	lineBuf := make([]byte, srcRow.RealSize+prefixRowHeaderLen)
	allLine := 0
	failConvertCount := 0

	defer func(start time.Time) {
		log.Printf("finished, all line:%d, failed line:%d, elapse time:%v", allLine, failConvertCount, time.Since(start))
	}(time.Now())

	go func() {
		for {
			time.Sleep(time.Minute)
			log.Printf("current all line:%d, failed convert count:%d ...", allLine, failConvertCount)
		}
	}()

	// 打开源文件
	srcFile, err := os.Open(srcFilename)
	if err != nil {
		return errors.WithStack(err)
	}
	defer srcFile.Close()

	// 打开目标文件
	dstFile, err := file.NewMyOutFile(dstFilename, bufSize)
	if err != nil {
		return err
	}
	defer dstFile.Close()

	// 按照列信息去取行数据
	for {
		// 读取一行数据
		readN, err := srcFile.Read(lineBuf)
		if err != nil {
			if err == io.EOF {
				break
			}
			return errors.Wrapf(err, "read src file")
		}

		if readN != srcRow.RealSize+prefixRowHeaderLen {
			return errors.Errorf("read failed, want:%d, got:%d", srcRow.RealSize+prefixRowHeaderLen, readN)
		}

		// read cols
		startPos := 0
		for i, v := range srcRow.Cols {
			if i > 0 {
				dstFile.Write(file.FieldsTerminated)
			}

			outStr, err2 := v.Convert(lineBuf[prefixRowHeaderLen+startPos:prefixRowHeaderLen+startPos+v.RealLen], from, to)
			if err2 != nil {
				if debug {
					log.Printf("ERROR:%v, line:%d, col:%s, reallen:%d", err2, allLine, v.Name, v.RealLen)
				}

				if failConvertCount < 10 { // 只打印前100条记录
					log.Printf("err:%v, line:%d, col:%s, reallen:%d,\n%s", err2, allLine, v.Name, v.RealLen, hex.Dump(lineBuf[prefixRowHeaderLen+startPos:prefixRowHeaderLen+startPos+v.RealLen]))
				}
				failConvertCount++

				if !skipErr {
					return errors.WithMessagef(err2, "line:%d", allLine)
				}
			}
			dstFile.Write([]byte(outStr))

			// 统一处理异常
			if dstFile.Err != nil {
				return dstFile.Err
			}

			startPos += v.RealLen
		}

		dstFile.Write(file.LinesTerminated)

		allLine++
	}

	return nil
}

func ConvertByDstSchema(srcFilename, dstFilename string, dstRow *other.Row) error {
	// 打开源文件
	srcFile, err := os.Open(srcFilename)
	if err != nil {
		return errors.WithStack(err)
	}
	defer srcFile.Close()
	scanner := file.NewScannerDelim(srcFile, bufSize)

	// 打开目标文件
	dstFile, err := file.NewMyOutFile(dstFilename, bufSize)
	if err != nil {
		return err
	}
	defer dstFile.Close()

	dstColNum := len(dstRow.Cols)
	allLine := 0
	failIncompLine := 0
	failConvertCount := 0

	defer func(start time.Time) {
		log.Printf("finished, all line:%d, incomplete line:%d, convert failed line:%d, elapse time:%v", allLine, failIncompLine, failConvertCount, time.Since(start))
	}(time.Now())

	go func() {
		for {
			time.Sleep(time.Minute)
			log.Printf("current all line:%d, incomplete line:%d, convert failed count:%d ...", allLine, failIncompLine, failConvertCount)
		}
	}()

	for scanner.Scan() {
		if prefixRowHeaderLen > 0 {
			dstFile.Write(prefixRowHeader)
		}
		row := bytes.Split(scanner.Bytes(), file.FieldsTerminated)
		if len(row) != dstColNum {
			if debug {
				log.Printf("ERROR: schema len:%d, dstfile column num:%d, %v, %s", len(row), dstColNum, allLine, scanner.Bytes())
			}
			failIncompLine++
			continue
		}

		for i, v := range dstRow.Cols {
			outBuf, err2 := v.ConvertFixSize(row[i], fromCode, toCode)
			if err2 != nil {
				if !skipErr {
					return errors.WithMessagef(err2, "line:%d", allLine)
				}
				if failConvertCount < 10 { // 只打印前几条记录
					log.Printf("err:%v, line:%d, col:%s, reallen:%d,\n%s", err2, allLine, v.Name, v.RealLen, hex.Dump(row[i]))
				}
				failConvertCount++
			}
			dstFile.Write(outBuf[:v.RealLen])
		}

		if dstFile.Err != nil {
			return dstFile.Err
		}

		allLine++
	}
	if scanner.Err() != nil {
		return scanner.Err()
	}

	return nil
}

func GetSchemaFromFile(filename string) (*other.Row, error) {
	schemaFile, err := os.Open(filename)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	defer schemaFile.Close()
	schemaScanner := bufio.NewScanner(schemaFile)

	var row = &other.Row{
		Cols: make([]other.Col, 0),
	}

	for schemaScanner.Scan() {
		record := schemaScanner.Bytes()
		var col other.Col
		if err := json.Unmarshal(record, &col); err != nil {
			return nil, errors.WithStack(err)
		}

		row.RealSize += col.RealLen
		row.Cols = append(row.Cols, col)
	}
	if err := schemaScanner.Err(); err != nil {
		return nil, errors.WithStack(err)
	}

	return row, nil
}

func main() {
	ExecuteIconv()
}
